import json
from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.serializers.json import DjangoJSONEncoder
from django.db import DatabaseError, transaction
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scope, scopes_disabled

from pretix.base.models import (
    Event,
    Invoice,
    InvoiceAddress,
    Item,
    ItemVariation,
    Order,
    OrderPosition,
    Organizer,
)
from pretix.base.models.orders import OrderFee
from pretix.base.services.invoices import (
    build_preview_invoice_pdf,
    generate_cancellation,
    generate_invoice,
    invoice_pdf_task,
    invoice_qualified,
    regenerate_invoice,
)
from pretix.base.services.orders import OrderChangeManager
from pretix.base.settings import GlobalSettingsObject


@pytest.fixture
def env():
    o = Organizer.objects.create(name='Dummy', slug='dummy')
    with scope(organizer=o):
        event = Event.objects.create(
            organizer=o,
            name='Dummy',
            slug='dummy',
            date_from=now(),
            plugins='pretix.plugins.banktransfer',
        )
        o = Order.objects.create(
            code='FOO',
            event=event,
            email='dummy@dummy.test',
            status=Order.STATUS_PENDING,
            datetime=now(),
            expires=now() + timedelta(days=10),
            total=0,
            locale='en',
        )
        tr = event.tax_rules.create(rate=Decimal('19.00'))
        o.fees.create(
            fee_type=OrderFee.FEE_TYPE_PAYMENT,
            value=Decimal('0.25'),
            tax_rate=Decimal('19.00'),
            tax_value=Decimal('0.05'),
            tax_rule=tr,
        )
        ticket = Item.objects.create(
            event=event,
            name='Early-bird ticket',
            category=None,
            default_price=23,
            tax_rule=tr,
            admission=True,
        )
        t_shirt = Item.objects.create(
            event=event,
            name='T-Shirt',
            category=None,
            default_price=42,
            tax_rule=tr,
            admission=True,
        )
        variation = ItemVariation.objects.create(value='M', item=t_shirt)
        OrderPosition.objects.create(
            order=o,
            item=ticket,
            variation=None,
            price=Decimal('23.00'),
            positionid=1,
        )
        OrderPosition.objects.create(
            order=o,
            item=t_shirt,
            variation=variation,
            price=Decimal('42.00'),
            positionid=2,
        )
        OrderPosition.objects.create(
            order=o,
            item=t_shirt,
            variation=variation,
            price=Decimal('42.00'),
            positionid=3,
            canceled=True,
        )
        gs = GlobalSettingsObject()
        gs.settings.ecb_rates_date = date.today()
        gs.settings.ecb_rates_dict = json.dumps(
            {
                'USD': '1.1648',
                'RON': '4.5638',
                'CZK': '26.024',
                'BGN': '1.9558',
                'HRK': '7.4098',
                'EUR': '1.0000',
                'NOK': '9.3525',
                'HUF': '305.15',
                'DKK': '7.4361',
                'PLN': '4.2408',
                'GBP': '0.89350',
                'SEK': '9.5883',
            },
            cls=DjangoJSONEncoder,
        )
        yield event, o


@pytest.mark.django_db
def test_locale_setting(env):
    event, order = env
    event.settings.set('invoice_language', 'de')
    with scopes_disabled():
        inv = generate_invoice(order)
    assert inv.locale == 'de'


@pytest.mark.django_db
def test_locale_user(env):
    event, order = env
    order.locale = 'en'
    event.settings.set('invoice_language', '__user__')
    inv = generate_invoice(order)
    assert inv.locale == order.locale


@pytest.mark.django_db
def test_address_old_country(env):
    event, order = env
    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='12345',
        city='London',
        country_old='England',
        country='',
        order=order,
    )
    inv = generate_invoice(order)
    assert inv.invoice_to == 'Acme Company\n221B Baker Street\n12345 London\nEngland'


@pytest.mark.django_db
def test_address_with_state(env):
    event, order = env
    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='46530',
        city='Granger',
        country=Country('US'),
        state='IN',
        order=order,
    )
    inv = generate_invoice(order)
    assert inv.invoice_to == 'Acme Company\n221B Baker Street\n46530 Granger IN\nUnited States of America'


@pytest.mark.django_db
def test_address_with_state_long(env):
    event, order = env
    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='46530',
        city='Granger',
        country=Country('MY'),
        state='10',
        order=order,
    )
    inv = generate_invoice(order)
    assert inv.invoice_to == 'Acme Company\n221B Baker Street\n46530 Granger Selangor\nMalaysia'


@pytest.mark.django_db
def test_address(env):
    event, order = env
    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='12345',
        city='London',
        country=Country('GB'),
        order=order,
    )
    inv = generate_invoice(order)
    assert inv.invoice_to == 'Acme Company\n221B Baker Street\n12345 London\nUnited Kingdom'


@pytest.mark.django_db
def test_address_vat_id(env):
    event, order = env
    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        name_parts={'full_name': 'Sherlock Holmes', '_scheme': 'full'},
        zipcode='12345',
        city='London',
        country_old='UK',
        country='',
        vat_id='UK1234567',
        order=order,
    )
    inv = generate_invoice(order)
    assert inv.invoice_to == 'Acme Company\nSherlock Holmes\n221B Baker Street\n12345 London\nUK\nVAT-ID: UK1234567'


@pytest.mark.django_db
def test_positions_skip_free(env):
    event, order = env
    event.settings.invoice_include_free = False
    op1 = order.positions.first()
    op1.price = Decimal('0.00')
    op1.save()
    inv = generate_invoice(order)
    assert inv.lines.count() == 2


@pytest.mark.django_db
def test_reverse_charge_note(env):
    event, order = env

    tr = event.tax_rules.first()
    tr.eu_reverse_charge = True
    tr.home_country = Country('DE')
    tr.save()

    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='12345',
        city='Warsaw',
        country=Country('PL'),
        vat_id='PL123456780',
        vat_id_validated=True,
        order=order,
        is_business=True,
    )

    ocm = OrderChangeManager(order, None)
    ocm.recalculate_taxes()
    ocm.commit()
    assert not order.positions.filter(tax_value__gt=0).exists()

    inv = generate_invoice(order)
    assert 'reverse charge' in inv.additional_text.lower()
    assert inv.foreign_currency_display == 'PLN'
    assert inv.foreign_currency_rate == Decimal('4.2408')
    assert inv.foreign_currency_rate_date == date.today()


@pytest.mark.django_db
def test_custom_tax_note(env):
    event, order = env

    tr = event.tax_rules.first()
    tr.eu_reverse_charge = True
    tr.home_country = Country('DE')
    tr.custom_rules = json.dumps(
        [
            {
                'country': 'PL',
                'address_type': '',
                'action': 'vat',
                'rate': '20',
                'invoice_text': {
                    'de': 'Polnische Steuer anwendbar',
                    'en': 'Polish tax applies',
                },
            }
        ]
    )
    tr.save()

    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='12345',
        city='Warsaw',
        country=Country('PL'),
        vat_id='PL123456780',
        vat_id_validated=True,
        order=order,
        is_business=True,
    )

    ocm = OrderChangeManager(order, None)
    ocm.recalculate_taxes()
    ocm.commit()

    inv = generate_invoice(order)
    assert 'Polish tax applies' in inv.additional_text


@pytest.mark.django_db
def test_reverse_charge_foreign_currency_data_too_old(env):
    event, order = env
    gs = GlobalSettingsObject()
    gs.settings.ecb_rates_date = date.today() - timedelta(days=14)

    tr = event.tax_rules.first()
    tr.eu_reverse_charge = True
    tr.home_country = Country('DE')
    tr.save()

    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='12345',
        city='Warsaw',
        country=Country('PL'),
        vat_id='PL123456780',
        vat_id_validated=True,
        order=order,
        is_business=True,
    )

    ocm = OrderChangeManager(order, None)
    ocm.recalculate_taxes()
    ocm.commit()
    assert not order.positions.filter(tax_value__gt=0).exists()

    inv = generate_invoice(order)
    assert 'reverse charge' in inv.additional_text.lower()
    assert inv.foreign_currency_rate is None
    assert inv.foreign_currency_rate_date is None


@pytest.mark.django_db
def test_reverse_charge_foreign_currency_disabvled(env):
    event, order = env
    event.settings.invoice_eu_currencies = False

    tr = event.tax_rules.first()
    tr.eu_reverse_charge = True
    tr.home_country = Country('DE')
    tr.save()

    event.settings.set('invoice_language', 'en')
    InvoiceAddress.objects.create(
        company='Acme Company',
        street='221B Baker Street',
        zipcode='12345',
        city='Warsaw',
        country=Country('PL'),
        vat_id='PL123456780',
        vat_id_validated=True,
        order=order,
        is_business=True,
    )

    ocm = OrderChangeManager(order, None)
    ocm.recalculate_taxes()
    ocm.commit()
    assert not order.positions.filter(tax_value__gt=0).exists()

    inv = generate_invoice(order)
    assert 'reverse charge' in inv.additional_text.lower()
    assert inv.foreign_currency_rate is None
    assert inv.foreign_currency_rate_date is None


@pytest.mark.django_db
def test_positions(env):
    event, order = env
    inv = generate_invoice(order)
    assert inv.lines.count() == 3
    first = inv.lines.first()
    assert 'Early-bird' in first.description
    assert first.gross_value == Decimal('23.00')

    second = inv.lines.all()[1]
    assert 'T-Shirt' in second.description
    assert 'M' in second.description
    assert second.gross_value == Decimal('42.00')

    last = inv.lines.last()
    assert 'Payment' in last.description
    fee = order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
    assert last.gross_value == fee.value
    assert last.tax_rate == fee.tax_rate
    assert last.tax_value == fee.tax_value
    assert inv.invoice_to == ''


@pytest.mark.django_db
def test_rebuilding(env):
    event, order = env
    inv = generate_invoice(order)
    inv2 = regenerate_invoice(inv)
    assert inv.order == inv2.order

    inv3 = generate_cancellation(inv)
    inv4 = regenerate_invoice(inv3)
    assert inv3.order == inv4.order


@pytest.mark.django_db
def test_cannot_delete_invoice(env):
    event, order = env
    inv = generate_invoice(order)
    with pytest.raises(Exception):
        inv.delete()


@pytest.mark.django_db
def test_cannot_write_invoice_without_order(env):
    event, _ = env
    with pytest.raises(Exception):
        i = Invoice(order=None, event=event)
        i.save()


@pytest.mark.django_db
def test_pdf_generation(env):
    event, order = env
    inv = generate_invoice(order)
    cancellation = generate_cancellation(inv)
    assert invoice_pdf_task(inv.pk)
    assert invoice_pdf_task(cancellation.pk)


@pytest.mark.django_db
def test_pdf_generation_custom_text(env):
    event, order = env
    event.settings.set('invoice_introductory_text', 'introductory invoice text')
    # set a really long additional text, to make the invoice span two pages
    event.settings.set('invoice_additional_text', 'additional invoice text\n' * 100)
    event.settings.set('show_date_to', False)
    inv = generate_invoice(order)
    assert invoice_pdf_task(inv.pk)


@pytest.mark.django_db
def test_pdf_preview_generation(env):
    event, order = env
    assert build_preview_invoice_pdf(event)


@pytest.mark.django_db
def test_invoice_numbers(env):
    event, order = env
    order2 = Order.objects.create(
        code='BAR',
        event=event,
        email='dummy2@dummy.test',
        status=Order.STATUS_PENDING,
        datetime=now(),
        expires=now() + timedelta(days=10),
        total=0,
        locale='en',
    )
    order2.fees.create(
        fee_type=OrderFee.FEE_TYPE_PAYMENT,
        value=Decimal('0.25'),
        tax_rate=Decimal('0.00'),
        tax_value=Decimal('0.00'),
    )
    testorder = Order.objects.create(
        code='BAR',
        event=event,
        email='dummy2@dummy.test',
        status=Order.STATUS_PENDING,
        datetime=now(),
        expires=now() + timedelta(days=10),
        total=0,
        testmode=True,
        locale='en',
    )
    inv1 = generate_invoice(order)
    inv2 = generate_invoice(order)
    invt1 = generate_invoice(testorder)

    event.settings.set('invoice_numbers_consecutive', False)
    inv3 = generate_invoice(order)
    inv4 = generate_invoice(order)
    inv21 = generate_invoice(order2)
    invt2 = generate_invoice(testorder)
    inv22 = generate_invoice(order2)

    event.settings.set('invoice_numbers_consecutive', True)
    inv5 = generate_invoice(order)
    inv6 = generate_invoice(order)
    invt3 = generate_invoice(testorder)
    inv7 = generate_invoice(order)
    Invoice.objects.filter(pk=inv6.pk).delete()  # This should never ever happen, but what if it happens anyway?
    inv8 = generate_invoice(order)
    inv23 = generate_invoice(order2)

    event.settings.set('invoice_numbers_counter_length', 6)
    inv24 = generate_invoice(order)
    event.settings.set('invoice_numbers_counter_length', 1)
    inv25 = generate_invoice(order)
    inv26 = generate_invoice(order)

    # expected behaviour for switching between numbering formats or dealing with gaps
    assert inv1.invoice_no == '00001'
    assert inv2.invoice_no == '00002'
    assert inv3.invoice_no == '{}-3'.format(order.code)
    assert inv4.invoice_no == '{}-4'.format(order.code)
    assert inv5.invoice_no == '00003'
    assert inv6.invoice_no == '00004'
    assert inv7.invoice_no == '00005'
    assert inv8.invoice_no == '00006'

    # test that separate orders are counted separately in this mode
    assert inv21.invoice_no == '{}-1'.format(order2.code)
    assert inv22.invoice_no == '{}-2'.format(order2.code)
    # but consecutively in this mode
    assert inv23.invoice_no == '00007'
    assert inv24.invoice_no == '000008'
    assert inv25.invoice_no == '9'
    assert inv26.invoice_no == '10'

    # test Invoice.number, too
    assert inv1.number == '{}-00001'.format(event.slug.upper())
    assert inv3.number == '{}-{}-3'.format(event.slug.upper(), order.code)

    assert invt1.number == '{}-TEST-00001'.format(event.slug.upper())
    assert invt2.number == '{}-TEST-{}-2'.format(event.slug.upper(), testorder.code)
    assert invt3.number == '{}-TEST-00002'.format(event.slug.upper())


@pytest.mark.django_db
def test_invoice_number_prefixes(env):
    event, order = env
    event2 = Event.objects.create(
        organizer=event.organizer,
        name='Dummy',
        slug='dummy2',
        date_from=now(),
        plugins='pretix.plugins.banktransfer',
    )
    order2 = Order.objects.create(
        event=event2,
        email='dummy2@dummy.test',
        status=Order.STATUS_PENDING,
        datetime=now(),
        expires=now() + timedelta(days=10),
        total=0,
        locale='en',
    )
    order2.fees.create(
        fee_type=OrderFee.FEE_TYPE_PAYMENT,
        value=Decimal('0.25'),
        tax_rate=Decimal('0.00'),
        tax_value=Decimal('0.00'),
    )
    event.settings.set('invoice_numbers_consecutive', False)
    event2.settings.set('invoice_numbers_consecutive', False)
    assert generate_invoice(order).number == 'DUMMY-{}-1'.format(order.code)
    assert generate_invoice(order2).number == 'DUMMY2-{}-1'.format(order2.code)

    event.settings.set('invoice_numbers_consecutive', True)
    event2.settings.set('invoice_numbers_consecutive', True)
    event.settings.set('invoice_numbers_prefix', '')
    event2.settings.set('invoice_numbers_prefix', '')

    assert generate_invoice(order).number == 'DUMMY-00001'
    assert generate_invoice(order).number == 'DUMMY-00002'
    assert generate_invoice(order2).number == 'DUMMY2-00001'
    assert generate_invoice(order2).number == 'DUMMY2-00002'

    event.settings.set('invoice_numbers_prefix', 'shared_')
    event2.settings.set('invoice_numbers_prefix', 'shared_')

    assert generate_invoice(order).number == 'shared_00001'
    assert generate_invoice(order2).number == 'shared_00002'
    assert generate_invoice(order).number == 'shared_00003'
    assert generate_invoice(order2).number == 'shared_00004'

    event.settings.set('invoice_numbers_consecutive', False)
    event2.settings.set('invoice_numbers_consecutive', False)
    assert generate_invoice(order).number == 'shared_{}-6'.format(order.code)
    assert generate_invoice(order2).number == 'shared_{}-6'.format(order2.code)

    event2.settings.set('invoice_numbers_prefix', 'inv_')
    event2.settings.set('invoice_numbers_prefix_cancellations', 'crd_')
    event2.settings.set('invoice_numbers_consecutive', True)
    event2.settings.set('invoice_numbers_counter_length', 4)
    i = generate_invoice(order2)
    assert i.number == 'inv_0001'
    assert generate_cancellation(i).number == 'crd_0001'

    event2.settings.set('invoice_numbers_prefix', 'inv_%Y%m%d_')
    i = generate_invoice(order2)
    assert i.number == 'inv_%s_0001' % now().date().strftime('%Y%m%d')

    # Test database uniqueness check
    with pytest.raises(DatabaseError):
        with transaction.atomic():
            Invoice.objects.create(
                order=order,
                event=order.event,
                organizer=order.event.organizer,
                date=now().date(),
                locale='en',
                invoice_no='00001',
            )


@pytest.mark.django_db
def test_sales_channels_qualify(env):
    event, order = env
    event.settings.set('invoice_generate', 'admin')

    # Orders with Total of 0 do never qualify
    assert invoice_qualified(order) is False

    order.total = Decimal('42.00')

    # Order with default Sales Channel (web)
    assert invoice_qualified(order) is True

    event.settings.set('invoice_generate_sales_channels', [])
    assert invoice_qualified(order) is False
